package com.xuecheng.media.service.impl;

import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.BusinessException;
import com.xuecheng.base.exception.CommonError;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.activation.MimeType;
import javax.annotation.Resource;
import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @description
 * @author Mr.M
 * @date 2022/9/10 8:58
 * @version 1.0
 */
 @Service
 @Slf4j
public class MediaFileServiceImpl extends ServiceImpl<MediaFilesMapper,MediaFiles>implements MediaFileService {

 @Resource
 MediaFilesMapper mediaFilesMapper;

 @Resource
 MinioClient minioClient;

 @Resource
 MediaProcessMapper mediaProcessMapper;

 /**
  * 注入当前类的代理对象
  */
 @Autowired
 MediaFileService currentProxy;

 @Value("${minio.bucket.files}")
 String FILE_BUCKET;

 @Value("${minio.bucket.videofiles}")
 String VIDEO_BUCKET;


    @Transactional
    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId,PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

  //构建查询条件对象
  LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
  
  //分页对象
  Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
  // 查询数据内容获得结果
  Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
  // 获取数据列表
  List<MediaFiles> list = pageResult.getRecords();
  // 获取数据总数
  long total = pageResult.getTotal();
  // 构建结果集
  PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
  return mediaListResult;

 }

 /**
  * 上传文件
  * @param companyId
  * @param uploadFileParamsDto
  * @param localFilePath
  * @param objectName 静态页面path
  * @return
  */
    @Override
    public UploadFileResultDto upload(Long  companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectName) {
     //==============================将文件上传MinIO==============================
     //获取拓展名
     String filename = uploadFileParamsDto.getFilename();
     String extension = filename.substring(filename.lastIndexOf('.'));
     String mimeType = getMimeType(extension);
     //构建对象名
     String defaultFolderPath = getDefaultFolderPath();
     String fileMd5 = getFileMd5(new File(localFilePath));
     //如果没传objectName就使用年月日目录
     if (objectName == null) {
      objectName = defaultFolderPath + fileMd5 + extension;
     }
     // todo BUG有重复上传重复保存的情况
     boolean res = addMediaFilesToMinIO(localFilePath, mimeType, FILE_BUCKET, objectName);
     if (!res) throw new BusinessException("上传文件失败");
     //==============================添加媒资信息到数据库==============================
     /*
     * 需要用代理对象来调用addMediaFilesToDb方法，不然事务不生效；加Transactional注解时要考虑：尽量加注解的方法内不要有网络请求的代码（除了数据库操作）
     * 因为网络请求的时间不固定，时长时短，会占用消耗我们的数据库资源，因为springboot的事务管理的底层也是基于数据库的事务实现的；
     * */
     MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, FILE_BUCKET, objectName);
     if (mediaFiles == null) {
      throw new BusinessException("文件上传后保存信息失败");
     }
     UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
     BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
     return uploadFileResultDto;
    }
 // todo BUG需要将课程基本信息的pic修改，不然页面不显示图片
 @Transactional
 @Override
 public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName) {
  MediaFiles files = getById(fileMd5);
  if (files == null) {
   MediaFiles mediaFiles = new MediaFiles();
   //拷贝基本信息
   BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
   mediaFiles.setId(fileMd5);
   mediaFiles.setCompanyId(companyId);
   mediaFiles.setBucket(bucket);
   mediaFiles.setFilePath(objectName);
   mediaFiles.setFileId(fileMd5);
   mediaFiles.setUrl("/"+bucket+"/"+objectName);
   mediaFiles.setCreateDate(LocalDateTime.now());
   mediaFiles.setStatus("1");
   mediaFiles.setAuditStatus("002003");
   boolean res = save(mediaFiles);
   // todo BUG 应区分视频文件与其他文件，应该只有视频文件才需要添加文件处理任务的记录
   //记录文件处理任务记录
   addWaitingTask(mediaFiles);
   if (!res) {
    log.error("文件基本信息保存失败，bucket:{},objectName:{}",bucket,objectName);
    return null;
   }
   return mediaFiles;
  }
  return files;
 }

 /**
  * 添加待处理任务
  * @param mediaFiles 媒资文件信息
  */
 private void addWaitingTask(MediaFiles mediaFiles){
  //得到文件类型
  String filename = mediaFiles.getFilename();
  String mimeType = getMimeType(filename.substring(filename.lastIndexOf(".")));
  //文件格式不为MP4的都需要处理
  if (!mimeType.equals("video/mp4")){
   MediaProcess mediaProcess = new MediaProcess();
   BeanUtils.copyProperties(mediaFiles,mediaProcess);
   mediaProcess.setStatus("1");
   mediaProcess.setUrl("");
   int res = mediaProcessMapper.insert(mediaProcess);
   if (res==0) BusinessException.cast(CommonError.UNKOWN_ERROR);
  }
 }
  /**
   * 检查文件是否存在
   * @param fileMd5 文件的md5
   * @return
   */
 @Override
 public RestResponse<Boolean> checkFile(String fileMd5) {
  if (StringUtils.isBlank(fileMd5)) {
   BusinessException.cast(CommonError.REQUEST_NULL);
  }
  //检查文件信息是否存在
  MediaFiles mediaFiles = getById(fileMd5);
  if (mediaFiles != null) {
   //文件所在的桶
   String bucket = mediaFiles.getBucket();
   //文件路径
   String filePath = mediaFiles.getFilePath();
   //查询minio是否存在该文件
   checkFile(filePath, bucket);
  }
  return RestResponse.success(false);
 }

 /**
  * 检查分块是否存在
  * @param fileMd5  文件的md5
  * @param chunkIndex  分块序号
  * @return
  */
 @Override
 public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
  //参数校验
  if (StringUtils.isBlank(fileMd5)){
   BusinessException.cast(CommonError.REQUEST_NULL);
  }
  //拼装分块路径
  String chunkPath = getChunkPath(fileMd5, chunkIndex);

  checkFile(chunkPath, VIDEO_BUCKET);
  return RestResponse.success(false);
 }

 /**
  * 上传分块
  * @param fileMd5  文件md5
  * @param chunk  分块序号
  * @param localChunkFilePath  分块文件本地路径
  * @return
  */
 @Override
 public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
  //得到源文件后缀
  String mimeType = getMimeType(null);
  //构建分块路径
  String chunkPath = getChunkPath(fileMd5, chunk);
  boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType,VIDEO_BUCKET,chunkPath);
  if (!b){
   return RestResponse.success(false,"上传文件失败");
  }
  return RestResponse.success(true);
 }

 /**
  * 合并文件
  * @param companyId  机构id
  * @param fileMd5  文件md5
  * @param chunkTotal 分块总和
  * @param uploadFileParamsDto 文件信息
  * @return
  */
 @Override
 public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
  //====================================分块文件合并==========================================
  //获取拓展名
  String filename = uploadFileParamsDto.getFilename();
  String fileType = filename.substring(filename.lastIndexOf("."));
  //获取objectName
  String objectName = getMergeFilePath(fileMd5, fileType);
  //获得分块文件目录
  String chunkDirPath = getChunkPath(fileMd5);
  List<ComposeSource> composeSources = Stream.iterate(0, i -> ++i)
          .limit(chunkTotal)
          .map(i -> ComposeSource.builder().bucket(VIDEO_BUCKET).object(chunkDirPath+i).build()).collect(Collectors.toList());
  //构建参数
  ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket(VIDEO_BUCKET)
          .object(objectName)
          .sources(composeSources).build();
  try {
   //合并文件
   minioClient.composeObject(composeObjectArgs);
  } catch (Exception e) {
   log.error("合并文件出错，bucket:{},objectName:{},错误信息:{}",VIDEO_BUCKET,objectName,e.getMessage());
   return RestResponse.validfail("合并失败");
  }
  //====================================校验合并文件与源文件是否相等==========================================
  //得到合并后的文件
  File mergeFile = downloadFileFromMinIO(VIDEO_BUCKET, objectName);
  try {
   String md5Hex = DigestUtil.md5Hex(mergeFile);
   //校验是否与源文件md5相等
   if (!md5Hex.equals(fileMd5)) {
    log.error("校验合并文件与源文件md5不相等");
    return RestResponse.validfail(false,"文件上传失败");
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
  //====================================将文件信息和文件处理任务记录添加到db中==========================================
  uploadFileParamsDto.setFileSize(mergeFile.length());
  MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, VIDEO_BUCKET, objectName);
  if (mediaFiles == null) {
   return RestResponse.validfail("文件信息入库失败");
  }
  //====================================清理分块文件==========================================
  clearChunkFiles(chunkDirPath,chunkTotal);
  return RestResponse.success(true);
 }

 /**
  * 清除分块文件
  * @param chunkFileFolderPath 分块文件路径
  * @param chunkTotal 分块文件总数
  */
 private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal) {
  try {
   List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
           .limit(chunkTotal)
           .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
           .collect(Collectors.toList());

   RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();
   Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
   results.forEach(r->{
    DeleteError deleteError = null;
    try {
     deleteError = r.get();
    } catch (Exception e) {
     e.printStackTrace();
     log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);
    }
   });
  } catch (Exception e) {
   e.printStackTrace();
   log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
  }
 }

  /**
   * 从minio下载文件
   * @param bucket 桶
   * @param objectName 对象名称
   * @return 下载后的文件
   */
 public File downloadFileFromMinIO(String bucket,String objectName){
  //临时文件
  File minioFile = null;
  FileOutputStream outputStream = null;
  try{
   InputStream stream = minioClient.getObject(GetObjectArgs.builder()
           .bucket(bucket)
           .object(objectName)
           .build());
   //创建临时文件
   minioFile=File.createTempFile("minio", ".merge");
   outputStream = new FileOutputStream(minioFile);
   IOUtils.copy(stream,outputStream);
   return minioFile;
  } catch (Exception e) {
   e.printStackTrace();
  }finally {
   if(outputStream!=null){
    try {
     outputStream.close();
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  }
  return null;
 }

 /**
  * 检查文件是否存在
  * @param chunkPath
  * @param video_bucket
  */
 private void checkFile(String chunkPath, String video_bucket) {
  GetObjectArgs build = GetObjectArgs.builder().bucket(video_bucket).object(chunkPath).build();
  try {
   FilterInputStream fileInputStream = minioClient.getObject(build);
   if (fileInputStream != null) {
    //说明文件在minio中存在
    RestResponse.success(true);
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 /**
  * 得到分块路径
  * @param fileMd5
  * @param chunkIndex
  */
 private String getChunkPath(String fileMd5, int chunkIndex) {
  return fileMd5.charAt(0)+"/"+ fileMd5.charAt(1)+"/"+fileMd5+"/"+"chunk"+"/"+ chunkIndex;
 }

 private String getChunkPath(String fileMd5) {
  return fileMd5.charAt(0)+"/"+ fileMd5.charAt(1)+"/"+fileMd5+"/"+"chunk"+"/";
 }

 /**
  * 得到合并文件目录
  * @param fileMd5
  * @return
  */
 private String getMergeFilePath(String fileMd5,String fileExt) {
  return fileMd5.charAt(0)+"/"+ fileMd5.charAt(1)+"/"+fileMd5+"/"+fileMd5+fileExt;
 }

 //获取文件的md5
 private String getFileMd5(File file) {
  try (FileInputStream fileInputStream = new FileInputStream(file)) {
   String fileMd5 = DigestUtils.md5Hex(fileInputStream);
   return fileMd5;
  } catch (Exception e) {
   e.printStackTrace();
   return null;
  }
 }



 //获取文件默认存储目录路径 年/月/日
 private String getDefaultFolderPath() {
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  String folder = sdf.format(new Date()).replace("-", "/")+"/";
  return folder;
 }


 /**
  * 根据后缀名得到文件类型
  * @param extension
  * @return
  */
 private String getMimeType(String extension){
       if(extension==null)
        extension = "";
       //根据扩展名取出mimeType
       ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
       //通用mimeType，字节流
       String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
       if(extensionMatch!=null){
        mimeType = extensionMatch.getMimeType();
       }
       return mimeType;
    }

 /**
  * 上传文件到minio
  * @param localFilePath
  * @param mimeType
  * @param bucket
  * @param objectName
  * @return
  */
 public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
  try {
   minioClient.uploadObject(UploadObjectArgs.builder()
           .bucket(bucket)
           .filename(localFilePath)
           .object(objectName)
           .contentType(mimeType)
           .build());
   return true;
  } catch (Exception e) {
   log.error(localFilePath+"上传失败！");
   e.printStackTrace();
  }
  return false;
 }

}
